通过重构实现模板方法模式(Template Method): 重构篇

context

现在公司的项目中,有两个界面表现几乎完全一样的控制器.如图:


但是两者的行为是部分有差别的,当时编码过程中,遵循了极限编程的原则,尽快的写出可以实现功能的代码. 重构和使用模式的工作放在以后进行 . 以防止过度设计带来的问题.
写好一个界面之后,直接复制代码,修改了部分.这在什么时候来看,都是编写的最快方式,但是却不是好的设计和可维护方案.

所以现在要开始进行重构了.

初始代码

下面是两个类之一的完整代码 .您不用完整阅读,因为在后面,我会逐一分解之:

GUIResetPasswordController.h文件

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>


@interface GUIResetPasswordController : UIViewController


@end

GUIPhoneNumberModificationController.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193

#import "GUIResetPasswordController.h"
#import "GUIChangePasswordController.h"
#import <SMS_SDK/SMS_SDK.h>
#import "GUIHeader.h"
#define kLeftViewWith (40)
#define kLeftViewHeight (40)

@interface GUIResetPasswordController ()


@property (nonatomic, weak) UITextField *phonenumberTextfield;
@property (nonatomic, weak) UITextField *verifyCodeTextfield;
@property (nonatomic, weak) UIButton *getVerifyCodeButton;
@property (nonatomic, weak) UIButton *submitButton;

@end
static NSInteger secondsCoutDown;
static NSTimer *countDownTimer;
@implementation GUIResetPasswordController


- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = kcMainGrayBackgroud;
[self setupChildViews];

}

- (void)setupChildViews {
UITextField *phonenumberTextfield = [[UITextField alloc]init];
phonenumberTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_dianhua"]];
phonenumberTextfield.leftViewMode = UITextFieldViewModeAlways;
phonenumberTextfield.placeholder = @"您的手机号";
phonenumberTextfield.backgroundColor = [UIColor whiteColor];
phonenumberTextfield.clearButtonMode = UITextFieldViewModeWhileEditing;
[self.view addSubview:phonenumberTextfield];
self.phonenumberTextfield = phonenumberTextfield;


UITextField *verifyCodeTextfield = [[UITextField alloc]init];
verifyCodeTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_yanzhengma"]];
verifyCodeTextfield.leftViewMode = UITextFieldViewModeAlways;
verifyCodeTextfield.placeholder = @"输入验证码";
verifyCodeTextfield.backgroundColor = [UIColor whiteColor];
[self.view addSubview:verifyCodeTextfield];
self.verifyCodeTextfield = verifyCodeTextfield;



UIButton *getVerifyCodeButton = [[UIButton alloc]init];
[getVerifyCodeButton setTitle:@"获取验证码" forState:UIControlStateNormal];
[getVerifyCodeButton setBackgroundColor:kcMainRed];
[getVerifyCodeButton setTitleColor:kcMainGrayBackgroud forState:UIControlStateNormal];
[getVerifyCodeButton addTarget:self action:@selector(getVerifyCode:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:getVerifyCodeButton];
self.getVerifyCodeButton = getVerifyCodeButton;

UIButton *submitButton = [[UIButton alloc] init];
[submitButton setTitle:@"下一步" forState:UIControlStateNormal];
[submitButton setBackgroundColor:[UIColor whiteColor]];
submitButton.layer.cornerRadius = 3;
[submitButton setTitleColor:kcMainRed forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(nextStep:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:submitButton];
self.submitButton = submitButton;
}

- (UIImageView *)textLeftViewWithImage:(UIImage *)leftImage {
UIImageView *containerView = [[UIImageView alloc] init];
UIImageView *iconView = [[UIImageView alloc]init];
iconView.image = leftImage;
[containerView addSubview:iconView];

UIImageView *seperatorView = [[UIImageView alloc]init];
seperatorView.backgroundColor = [UIColor redColor];
[containerView addSubview:seperatorView];
//set frame
containerView.bounds = CGRectMake(0, 0, kLeftViewWith+20, kLeftViewWith);

CGFloat iconViewWH = 20;
CGFloat iconViewX = (kLeftViewWith - iconViewWH)/2;
CGFloat iconViewY = (kLeftViewHeight - iconViewWH)/2;

iconView.frame = CGRectMake(iconViewX, iconViewY, iconViewWH, iconViewWH);

CGFloat seperatorW = 1;
CGFloat seperatorH = 20;
CGFloat seperatorX = kLeftViewWith-seperatorW;
CGFloat seperatorY = (kLeftViewHeight-seperatorH)/2;

seperatorView.frame = CGRectMake(seperatorX, seperatorY, seperatorW, seperatorH);

return containerView;
}

- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];

WSE(ws);

static const CGFloat kTextFieldHeight = 50;
[self.phonenumberTextfield mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(ws.view);
make.leading.mas_equalTo(ws.view);
make.trailing.mas_equalTo(ws.view);
make.height.mas_equalTo(kTextFieldHeight);
}];

[self.verifyCodeTextfield mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(ws.phonenumberTextfield.mas_bottom).with.offset(1);
make.leading.mas_equalTo(ws.view);
make.trailing.mas_equalTo(ws.view).with.offset(-GUIScreenWidth/3);
make.height.mas_equalTo(kTextFieldHeight);
}];

[self.getVerifyCodeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(GUIScreenWidth/3, kTextFieldHeight));
make.leading.mas_equalTo(ws.verifyCodeTextfield.mas_trailing);
make.bottom.mas_equalTo(ws.verifyCodeTextfield);
}];

[self.submitButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(GUIScreenWidth/3, 30));
make.leading.mas_equalTo((GUIScreenWidth-(GUIScreenWidth/3))/2);
make.top.mas_equalTo(ws.verifyCodeTextfield.mas_bottom).with.offset(60);
}];


}

- (void)nextStep:(UIButton *)button {

[SMS_SDK commitVerifyCode:self.verifyCodeTextfield.text result:^(enum SMS_ResponseState state) {
if (state == SMS_ResponseStateSuccess) {
[MBProgressHUD showMessage:@"正在验证 ... "];
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];
[MBProgressHUD hideHUD];
} else {
[MBProgressHUD showError:@"验证失败"];
}
}];
#warning temp code ,remember to delete
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];

}

#pragma mark - share sdk sms
- (void)getVerifyCode:(UIButton *)button {

if (self.phonenumberTextfield.text == nil) {
[MBProgressHUD showError:@"请先填写手机号码"];
return;
}


NSString *phoneNumber = self.phonenumberTextfield.text;

#warning temp code ,remember to delete
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [paths lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"resetPhoneNumber.plist"];
[phoneNumber writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

[SMS_SDK getVerificationCodeBySMSWithPhone:phoneNumber zone:@"86" result:^(SMS_SDKError *error) {
//save phone number to file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [paths lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"resetPhoneNumber.plist"];
[phoneNumber writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}];

[self getTheCodeAgain];
}

- (void)getTheCodeAgain {

secondsCoutDown = 60;
self.getVerifyCodeButton.enabled = NO;
countDownTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeDown) userInfo:nil repeats:YES];

}

- (void)timeDown {
secondsCoutDown--;
if (secondsCoutDown == 0) {
[countDownTimer invalidate];
self.getVerifyCodeButton.enabled = YES;
}
[self.getVerifyCodeButton setTitle:[NSString stringWithFormat:@"请等待 %ld 秒", (long)secondsCoutDown] forState:UIControlStateDisabled];
}

@end

重构代码

发现代码的问题

  • 首先,文件本身没有结构,阅读需要翻来翻去. 利用 Code Snippets 添加固定结构到文件顶部:
1
2
3
4
5
6
7
8
9
10
11


#pragma mark -life cycle

#pragma mark - <#name#> delegate

#pragma mark -event response

#pragma mark -private methods

#pragma mark -getters and setters

方法分门别类放好,最后可以从这里看到代码的结构了.

关于这么组织的点子和原因,来源于这篇文章

我要还补充一点,就是对使用 xvim 插件的开发者们, 这方法是在爽爆了,当你在 viewDidLoad 中编写代码,需要一个新方法的时候,只需要:

  1. 切换到命令模式 (我已经映射成了 zz)
  2. / 输入你想写的方法的 mark ,比如 private,回车
  3. o

一个熟练的 vim 使用者,整个过程也就2秒钟 ,那代码在你眼前飞动的感觉呦 ~

下面回归正题

代码的不合理之处

从第一个方法 viewDidLoad 开始往下看

1
2
3
4
5
6
7
#pragma mark -life cycle
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = kcMainGrayBackgroud;
[self setupChildViews];

}

似乎没有什么问题, 如果有强迫症可以把设置背景色提取一个方法.提取方法的原则是:你想给代码写注释了 ,那么就把代码放到方法中,给方法起个意图明显的名字,哪怕只有一行代码 ! (因为注释都是邪恶的,不解释)

但是我觉得这里没有什么阅读困难,所以不去提取方法.

然后下一行 [self setupChildViews];,点击进去. okay,出现问题代码了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

- (void)setupChildViews {
UITextField *phonenumberTextfield = [[UITextField alloc]init];
phonenumberTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_dianhua"]];
phonenumberTextfield.leftViewMode = UITextFieldViewModeAlways;
phonenumberTextfield.placeholder = @"您的手机号";
phonenumberTextfield.backgroundColor = [UIColor whiteColor];
phonenumberTextfield.clearButtonMode = UITextFieldViewModeWhileEditing;
[self.view addSubview:phonenumberTextfield];
self.phonenumberTextfield = phonenumberTextfield;


UITextField *verifyCodeTextfield = [[UITextField alloc]init];
verifyCodeTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_yanzhengma"]];
verifyCodeTextfield.leftViewMode = UITextFieldViewModeAlways;
verifyCodeTextfield.placeholder = @"输入验证码";
verifyCodeTextfield.backgroundColor = [UIColor whiteColor];
[self.view addSubview:verifyCodeTextfield];
self.verifyCodeTextfield = verifyCodeTextfield;



UIButton *getVerifyCodeButton = [[UIButton alloc]init];
[getVerifyCodeButton setTitle:@"获取验证码" forState:UIControlStateNormal];
[getVerifyCodeButton setBackgroundColor:kcMainRed];
[getVerifyCodeButton setTitleColor:kcMainGrayBackgroud forState:UIControlStateNormal];
[getVerifyCodeButton addTarget:self action:@selector(getVerifyCode:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:getVerifyCodeButton];
self.getVerifyCodeButton = getVerifyCodeButton;

UIButton *submitButton = [[UIButton alloc] init];
[submitButton setTitle:@"下一步" forState:UIControlStateNormal];
[submitButton setBackgroundColor:[UIColor whiteColor]];
submitButton.layer.cornerRadius = 3;
[submitButton setTitleColor:kcMainRed forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(nextStep:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:submitButton];
self.submitButton = submitButton;
}

这个方法违背了单一职责原则.就是一个方法做了太多事情. 下面通过 提取方法 的重构手法进行重构.

最终代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

- (void)setupChildViews {

[self setupPhoneNumberTextField];

[self setupVerifiedCodeTextField];

[self setupGetVerifiyCodeButton];

[self setupSubmitButton];


}

- (void)setupPhoneNumberTextField {

UITextField *phonenumberTextfield = [[UITextField alloc]init];
phonenumberTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_dianhua"]];
phonenumberTextfield.leftViewMode = UITextFieldViewModeAlways;
phonenumberTextfield.placeholder = @"您的手机号";
phonenumberTextfield.backgroundColor = [UIColor whiteColor];
phonenumberTextfield.clearButtonMode = UITextFieldViewModeWhileEditing;
[self.view addSubview:phonenumberTextfield];
self.phonenumberTextfield = phonenumberTextfield;

}

- (void)setupVerifiedCodeTextField {

UITextField *verifyCodeTextfield = [[UITextField alloc]init];
verifyCodeTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_yanzhengma"]];
verifyCodeTextfield.leftViewMode = UITextFieldViewModeAlways;
verifyCodeTextfield.placeholder = @"输入验证码";
verifyCodeTextfield.backgroundColor = [UIColor whiteColor];
[self.view addSubview:verifyCodeTextfield];
self.verifyCodeTextfield = verifyCodeTextfield;


}

- (void)setupGetVerifiyCodeButton {

UIButton *getVerifyCodeButton = [[UIButton alloc]init];
[getVerifyCodeButton setTitle:@"获取验证码" forState:UIControlStateNormal];
[getVerifyCodeButton setBackgroundColor:kcMainRed];
[getVerifyCodeButton setTitleColor:kcMainGrayBackgroud forState:UIControlStateNormal];
[getVerifyCodeButton addTarget:self action:@selector(getVerifyCode:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:getVerifyCodeButton];
self.getVerifyCodeButton = getVerifyCodeButton;

}

- (void)setupSubmitButton {

UIButton *submitButton = [[UIButton alloc] init];
[submitButton setTitle:@"下一步" forState:UIControlStateNormal];
[submitButton setBackgroundColor:[UIColor whiteColor]];
submitButton.layer.cornerRadius = 3;
[submitButton setTitleColor:kcMainRed forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(nextStep:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:submitButton];
self.submitButton = submitButton;

}

关于对变量命名的改进,我待会自己做了,就不写出来了

通过重构,整个结构变得清爽. 但是有出现了一个新问题: 在 viewDidLoad中对 setupChildViews 的调用显得有点不平衡(个人觉得),更重要的原因,以后我要采用模板方法这个模式,过深的调用层级,子类重写方法的时候,需要开发者过多精力.所以,我采用 内联方法将函数的实现放到调用函数的地方.

删除 setupChildViews ,将实现放到 viewDidLoad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = kcMainGrayBackgroud;

[self setupPhoneNumberTextField];

[self setupVerifiedCodeTextField];

[self setupGetVerifiyCodeButton];

[self setupSubmitButton];


}

到此,我觉得方法就这样吧. 接下来找变量的问题. 第一眼看到的就是两个宏:

1
2
#define kLeftViewWidth   (40)
#define kLeftViewHeight (40)

记得在哪里读过 ,在值替换上使用宏是一个不好的习惯(宏有自己不可替换的功能,这里不做讨论).因为有全局变量可以实现它的功能,更重要的是编译器会对全局变量的类型等进行检查,降低出错的几率.

所以修改如下:

1
2
static CGFloat const kLeftViewHeight = 40;
static CGFloat const kLeftViewWidth = 40;

结语

好了,重构篇就先到此了, 可以肯定的是,这样去做模板方法模式,是不够的,因为没有隔离出变化.但是我准备把这项工作放到下一篇中,毕竟要先遇到问题,再去解决问题才是好的做法